Cross-site Scripting

Contributor

Tianhao Ma, Yingzhe Zhang

How to run the application

environment and preparation

  1. python 2.7
  2. pip install webapp2
  3. pip install flask
  4. pip install flask-csp 

how to run

  1. level1
    • csp1.0
    • python2.7 app_csp1.py
    • http://127.0.0.1:8080/
    • csp2.0
    • python2.7 app_csp2.py
    • http://127.0.0.1:8080/
  2. level2
    • python2.7 app.py
    • csp1.0
    • http://127.0.0.1:8080/csp1
    • csp2.0
    • http://127.0.0.1:8080/csp2
  3. level3
    • python2.7 app.py
    • csp1.0
    • http://127.0.0.1:8080/csp1#1
    • csp2.0
    • http://127.0.0.1:8080/csp2#1
  4. level4
    • python2.7 app.py
    • http://127.0.0.1:8080/
  5. level5
    • python2.7 app.py
    • http://127.0.0.1:8080/
  6. level6
    • python2.7 app.py
    • http://127.0.0.1:8080/csp1
    • http://127.0.0.1:8080/csp2

XSS Exploit Generation

Level 1: Hello, world of XSS

(1) where the vulnerable source code locates:
we have a input:
1-1
Then it is expected that a text should be typed here and the website will respond the text to user as follow:
1-2
But what if we input javascript code instead of plain text? we can see the code:
1-3
This is because the original code does not filter the javascript code and allow the javascript inject here and execute when it return to the html, which causes injection vulnerability:
1-5

(2) how to trigger vulnerability:
So if we use: <script>alert("Hello world!!!")</script>here, the alert will appear here:
1-4

Level 2: Persistence is key

(1) Where the vulnerable source code locates:
We can input some text in the text box and then the text will be shown in the post list.

L2_input

First we try to directly input the <script>alert("Hello World")</script> in the text box. Then the new post is shown but the alert not. So we go into the source code and I find the following sentence.

L2_code

post[i].message is the text of our input and it will be added to the html. Finally, the html will be added to containerEl.innerHTML. If the script is dynamically embedded by innerHTML, the browser will treat it as normal text and will not maintain it as a script node in the DOM. So it cannot be found when called.

(2) How to trigger vulnerability:

There is an easy way. Since the data is stored on the server side. We can use the onerror event, and the function that handles the error will be called. In this way, we can put the alert into the function. For instance, we can write a post like this:

<img src="cannot_find_img.png" onerror=alert("Yingzhe")>.

The server cannot find the image so there will be an error and the onerror function will be called. Then we can see the alert.

L2_alert

Level 3: That sinking Feeling...

(1) where the vulnerable source code locates:
we have a website which contains three tab that users can click and show the picture below it:
2-1
and we can check the code from console of browser and find the number in url after # is showed here:
2-2
So if we put some text in url instead of number, we will find:
2-3
This is because the original code directly let the text execute in the code rather than filter it:
2-7

(2) how to trigger vulnerability:
So if we put some javascript in Url after #:
2-4
and the code will be interpreted here:
2-5
and this will make alert pops up:
2-6

Level 4: Context matters

(1) Where the vulnerable source code locates:

We can input a number to the timer and then the timer start. At the same time we can see the number is added to the parameters in the URL.

L4_input

L4_para

Let's go into the source code. On the python side, our input will be saved in the timer and then the timer.html will be rendered using the timer.

L4_pythonside

And in the timer.html, the startTimer() function will be called when the page is loading. As we can see, '{{ timer }}' is the parameter and we can treat that as a string.

L4_timerfun

(2) How to trigger vulnerability:

Since we can treat the timer parameter as a string, we can include') in our input to end the startTimer() first and then add the alert . Then whole input looks like this:

3');alert('Yingzhe.

When the page loading, the following will be called. startTimer(3); alert('Yingzhe');. So we can see the alert.

L4_alert

Level 5: Breaking protocol

(1) where the vulnerable source code locates:
when we first get into website, we find this:
3-1
This is because:
3-2
when we click this link, we are brought to here:
3-3
and the code will get the value of next which is comfirm and set this value to next,then render the html signup.html.
3-4
when we get into the signup.html, the nextvalue will be set here. So what if we directly type the url https://xss-game.appspot.com/level5/frame/signup?next=mario and set next=mario, we can get:3-5

(2) how to trigger vulnerability:
So if we inject the code like this https://xss-game.appspot.com/level5/frame/signup?next=javascript:alert('mario'), the alert will pop up:
3-6

Level 6: Follow the 🐇

(1) Where the vulnerable source code locates:
We can change the string after # in the URL. If we change that to www.google.com, we can see the result in the page.
L6_UR

So it looks like the page will load the URL. Let's take a look at the source code.
L6_sourcecode

The function will create a <script> element and put the content of the URL into the <script>.

(2) How to trigger vulnerability:

Actually we can write the alert() into a JavaScript file but there is a easy way: use the Data URLs

Data URLs

URLs prefixed with the data: scheme, allow content creators to embed small files inline in documents.

data:[<mediatype>][;base64],<data>

So we can input the URL like this:

https://xss-game.appspot.com/level6/frame#data:text/plain,alert("Yingzhe")

<mediatype> is text/plain and <data> is alert("Yingzhe").

So we can see the alert.
L6_alert

Since regular expressions are case-sensitive, we can also type that http as HTtp to bypass the check. Or type a space' ' before the http.

Vulnerability Patching

Level 1: Hello, world of XSS

Since we can input any text and may interpret them as javascript code, which will bring injection vulnerability, I write a method named 'escapeHtml' which uses the dependency cgi to do the string escape. Finally can interpret the code as normal text even the input string is javascript code.
1-1patch

Level 2: Persistence is key

First we create a server using python flask. It will send the CSP header to the client(in next step of the home work) and render the html. In the JavaScript side, we add a encodeText() function to encode the input as the plain text.

patching_level2_1

The logic is creating a <div> and putting the text in its innerText (for Firefox is textContent). And then we extract the innerHTML of the div as the plain text. Before we put the input of the user to the html, we encode the post[].message as plain text. The page finally displays the original text typed by the user.
patching_level2_2

Cause the first post is a Welcome Message , we will skip it and then encode the following post. In this case, the result will be
patching_level2_3

Level 3: That sinking Feeling...

Since the attack happens in the client-end, we just fix that in html.
3-1patch
As the picture shows that there are two ways to defend injection. The first is that write a function named 'html2Escape' to escape the text from html input, which supports the escaping as follow:

Code Escape
< &lt
> &gt
& &amp
" &quot

Then if we input javascript code ><img src=x onerror="alert("mario")", the function help us interpret it as &gt&ltimg src=x onerror=&quotalert(&quotmario&quot)&quot, which will prevent injection vulnerability.
Then let us talk about other way. Through analysing the code, we know the function receives an argument whose type is a number(int). So we can first transfer the argument and parse it into an integer,then uses isNaN to judge if the result is an integer. If we get the result is True, we directly end the method. For example, if we set the argument number='mario', the parseInt method produce NaN with this argument. Then we judge the NaN and get the result True and end the process.
By the way, after moving the javascript code into a js file, we need import it into html.
3-2patch

Level 4: Context matters

This time we will check the user's input on the sever side. We create a check_timer() function to get the submission value of the timer and check if it is a number using isdigit(). The sever will return the right page only when the user input a valid value.
patching_level4_1

On the client side, the only change is that we add the check_timer as a GET mothed to the action in the <form> element. The client will send the request including the timer value to the sever.
patching_level4_2

In this case, if the input is not a number, the sever will return a error page to the client.
patching_level4_3

When we download the source code and run it locally, we find if we put the JavaScript code in the<head> the selector cannot find the element with its id because the page has not finished loading. So we put the code of selector in the <body>. And in the next CSP step, we put that in a JS file.
patching_level4_4

Level 5: Breaking protocol

Since Using the javascript:alert('mario') and http:// can cause the vulnerability, I decide to filter this two type request.
5-1patch
First by checking if javascript exists in request path and using regular expression to judge the malicious code, we can detect the injection vulnerability. In this circumstance, I direct the request to the page hacker_fail.html, which effectively prevent injection vulnerability.
However, I this solution is very tricky. If we are sure that the next step is going to "confirm.html", we can write this hard code to make sure we always get the right path no matter what the input arguments are(I state this circumstance in comment).

Level 6: Follow the 🐇

Due to the protocol limitation, the content behind the # of the URL cannot be included in the request, so we cannot check the input in the address bar on the server side. So the main problem is to modify the regular expression part.

When we get the content after #, we will delete all the space ' ' in that using Regex.
patching_level6_1

Double backslash symbol // is another way to use https or http, so the next step is check wether the content starting with //.
patching_level6_2

After that, get the first 8 character of the content as the urlHead and transfer that to lowercase. In these way, we can check if the url uses HTTP, data url, FTP, SMTP and other protocols. Once it is detected that it uses the above protocol, an error message will be displayed.
patching_level6_3

The result is as follows,
patching_level6_4

Defense via Content Security Policy

Level 1: Hello, world of XSS

csp 1.0

  csp_json = {
            "script-src": "https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js 'self'",
            "img-src": "https://xss-game.appspot.com/static/logos/level1.png",
            "default-src": "self",
            "style-src": "https://xss-game.appspot.com/static/game-frame-styles.css",
        }

csp 2.0

use sha256 to handle inline js code

 csp_json = {
            "script-src": "https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js "
                          "'sha256-h6wTjHUH5feO5x5t9UyBsoZr6dMUtn9mb9wHpKUf7Uw='",
            "img-src": "https://xss-game.appspot.com/static/logos/level1.png",
            "default-src": "self",
            "style-src": "https://xss-game.appspot.com/static/game-frame-styles.css",
        }

Level 2: Persistence is key

csp 1.0

@csp_header({
    "default-src": "'self'",
    "script-src": "'self' https://xss-game.appspot.com/static/post-store.js",
    "img-src": "https://xss-game.appspot.com/static/logos/level2.png "
               "https://xss-game.appspot.com/static/level2_icon.png "
               "https://ssl.gstatic.com/s2/oz/images/sprites/stream-e001443aa61c5529c1aa133a9c12bb49.png",
    "style-src": "https://xss-game.appspot.com/static/game-frame-styles.css",
    "report-uri": ""
})

csp 2.0

use nonce to handle inline js code

csp_json = {
        "default-src": "'self'",
        'script-src': "'self' https://xss-game.appspot.com/static/post-store.js 'nonce-" + nonce + "'",
        "img-src": "https://xss-game.appspot.com/static/logos/level2.png "
                   "https://xss-game.appspot.com/static/level2_icon.png "
                   "https://ssl.gstatic.com/s2/oz/images/sprites/stream-e001443aa61c5529c1aa133a9c12bb49.png",
        "style-src": "https://xss-game.appspot.com/static/game-frame-styles.css",
        "report-uri": ""
    }

The function of generating random nonce is

import hashlib
m = hashlib.sha256("hello")
# generate random nonce
def generate_nonce():
    global m
    m.update("hello world")
    return m.hexdigest()

Level 3: That sinking Feeling...

csp 1.0

@csp_header({'script-src': "'self' http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js",
             "img-src": "https://xss-game.appspot.com/static/logos/level3.png "
                        "https://xss-game.appspot.com/static/level3/cloud1.jpg "
                        "https://xss-game.appspot.com/static/level3/cloud2.jpg "
                        "https://xss-game.appspot.com/static/level3/cloud3.jpg",
             "style-src": "https://xss-game.appspot.com/static/game-frame-styles.css",
             "report-uri": ""})

csp 2.0

use random nonce- to handle inline js code.

csp_json = {'script-src': "'self' http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js "
                              "'nonce-" + nonce + "'",
                "img-src": "https://xss-game.appspot.com/static/logos/level3.png "
                           "https://xss-game.appspot.com/static/level3/cloud1.jpg "
                           "https://xss-game.appspot.com/static/level3/cloud2.jpg "
                           "https://xss-game.appspot.com/static/level3/cloud3.jpg",
                "style-src": "https://xss-game.appspot.com/static/game-frame-styles.css",
                "report-uri": ""
                }

the method of generating random nonce

import hashlib
m = hashlib.sha256("hello")
# generate random nonce
def generate_nonce():
    global m
    m.update("hello world")
    return m.hexdigest()
nonce = generate_nonce()

Level 4: Context matters

csp 1.0

@csp_header({
    "default-src": "'self'",
    "script-src": "'self' https://xss-game.appspot.com/static/game-frame.js",
    "img-src": "https://xss-game.appspot.com/static/logos/level4.png https://xss-game.appspot.com/static/loading.gif",
    "style-src": "https://xss-game.appspot.com/static/game-frame-styles.css",
    "report-uri": ""
})

csp 2.0

use random nonce- to handle inline js code.

csp_json = {"default-src": "'self'",
                "script-src": "'self' https://xss-game.appspot.com/static/game-frame.js 'nonce-" + nonce + "'",
                "img-src": "https://xss-game.appspot.com/static/logos/level4.png "
                           "https://xss-game.appspot.com/static/loading.gif",
                "style-src": "https://xss-game.appspot.com/static/game-frame-styles.css",
                "report-uri": ""
                }

the method of generating random nonce

import hashlib
m = hashlib.sha256("hello")
# generate random nonce
def generate_nonce():
    global m
    m.update("hello world")
    return m.hexdigest()

Level 5: Breaking protocol

csp 1.0

csp_json = {
    "script-src": "'self' http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js ",
    "img-src": "https://xss-game.appspot.com/static/logos/level5.png ",
    "default-src": "self",
    "style-src": "https://xss-game.appspot.com/static/game-frame-styles.css",
    "report-uri": "",
}

csp 2.0

use sha256 to handle inline js code

csp_json = {
    "script-src": "'self' http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js "
                  "'sha256-cN25VmyGJVDLdpkS+JoZ3jMxtke1QdPN8mucaJj/2bc='",
    "img-src": "https://xss-game.appspot.com/static/logos/level5.png ",
    "default-src": "self",
    "style-src": "https://xss-game.appspot.com/static/game-frame-styles.css",
    "report-uri": "",
}

Level 6: Follow the 🐇

csp 1.0

@csp_header({
    "default-src": "'self'",
    "script-src": "'self' https://xss-game.appspot.com/static/game-frame.js",
    "img-src": "https://xss-game.appspot.com/static/level6_cube.png "
               "https://xss-game.appspot.com/static/logos/level6.png ",
    "style-src": "https://xss-game.appspot.com/static/game-frame-styles.css",
    "report-uri": ""
})

csp 2.0

use nonce to handle inline js code.

csp_json = {"default-src": "'self'",
                "script-src": "'self' https://xss-game.appspot.com/static/game-frame.js 'nonce-" + nonce + "'",
                "img-src": "https://xss-game.appspot.com/static/level6_cube.png "
                           "https://xss-game.appspot.com/static/logos/level6.png ",
                "style-src": "https://xss-game.appspot.com/static/game-frame-styles.css",
                "report-uri": ""
                }

the method of generating random nonce

import hashlib
m = hashlib.sha256("hello")
# generate random nonce
def generate_nonce():
    global m
    m.update("hello world")
    return m.hexdigest()